Dagger2 入门详解

什么是Dagger2

Dagger2是Android中比较热门依赖注入框架,而Android开发当前非常流行框架就是MVP模式了,Dagger2的目标便是将MVP中的V和P 进一步解耦,达到模块化最大的解耦,使得代码更容易维护。

Google官方主页介绍

Dagger is a fully static, compile-time dependency injection framework for both Java and Android. It is an adaptation of an earlier version created by Square and now maintained by Google.
Dagger aims to address many of the development and performance issues that have plagued reflection-based solutions. More details can be found in this talk(slides) by +Gregory Kick.

翻译理解

Dagger是为Android和Java平台提供的一个完全静态的,在编译时进行依赖注入的框架。 早期的版本由Square公司进行改编,现在由Google进行维护。
Dagger旨在解决基于反射带来的开发和性能上的问题(因为Dagger并没有用反射来做依赖注入)。更多详细内容可以看一下Gregory Kicktalk这个人的talk

通俗理解

其实就是告诉我们Dagger可以用来进行依赖注入,搞过Spring的人肯定都知道这是啥,SpringMVC里用到了大量依赖注入,下面聊一聊依赖注入

什么是依赖注入

维基百科

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来降低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调度者调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。

通俗理解

当一个类中需要依赖其他对象时,不需要你亲自为那些需要依赖的对象赋值,为那些对象赋值的操作交给了IOC框架(如Dagger2)。例如,我们在做项目时,经常需要在一个对象里去创建另一个对象的示例,这种行为是产生耦合的常见形式,对于一个大型项目来说,过多的相互依赖会导致代码难以维护,很容易就会碰到修改一个小需求需要大面积的修改各种代码,特别是代码原来不是自己维护的,麻烦会更大。

小例子

有一家咖啡店CoffeeShop,向顾客提供咖啡,我们非常容易明白咖啡店CoffeeShop是不能自己生产咖啡给顾客的,他们只能通过不同咖啡制作方法来生产咖啡供顾客饮用,此时抽象一个接口CoffeeMaker。

传统做法

  • 业务核心类是CoffeeShop,通过CoffeeMaker负责生产Coffee并进行售卖

    /**

    • 这是一个售卖Coffee的例子
    • CoffeeMaker是对生成Coffee过程的一个封装
    • 制作Coffee需要实现CoffeeMaker的produceCoffee方法
      */
      public class CoffeeShop {

      private CoffeeMaker mCoffeeMachine;

      public CoffeeShop(CoffeeMaker coffeeMachine) {

      mCoffeeMachine = new SimpleCoffeeMachine();
      

      }

      public String provideCoffee() {

      return mCoffeeMachine.produceCoffee();
      

      }
      }

  • Coffee的实际生产接口CoffeeMaker

    public interface CoffeeMaker {

    String produceCoffee(); // 实际生成咖啡的方法
    

    }

  • 实际的Coffee生产者,下面是简单的咖啡机SimpleCoffeeMachine

    public class SimpleCoffeeMachine implements CoffeeMaker {

    @Override
    public String produceCoffee() {
        return "Coffee is made by SimpleCoffeeMachine";
    }
    

    }

刚开始咖啡都是咖啡机做出来的,这家咖啡店很简单,在CoffeeShop中可以看到,CoffeeShop持有了一个CoffeeMaker生产接口,而具体制作Coffee的过程是由实现了CoffeeMaker的自动咖啡机SimpleCoffeeMachine实现的,CoffeeMaker是在构造方法中new 出了一个实现CoffeeMaker接口的SimpleCoffeeMachine。当前的功能很简单,这么写看着也没什么问题。

随着业务的扩展,消费人群改变了,自动咖啡机SimpleCoffeeMachine也完全不能满足现有客户的需求,这个时候我们的CoffeeShop该进行业务升级。

  • 雇佣咖啡师CoffeeCooker来制作咖啡。

    public class CoffeeCooker {

    private String mName; // 咖啡师名字
    private String MCoffeeType; // 生产的Coffee类型
    
    public CoffeeCooker(String name, String MCoffeeType) {
        mName = name;
        this.MCoffeeType = MCoffeeType;
    }
    
    public String produceCoffee() {
        return mName + "制作了" + MCoffeeType + "的Coffee.";
    }
    

    }

  • 此时要升级SimpleCoffeeMachine了

    public class SimpleCoffeeMachine implements CoffeeMaker {

    // @Override
    // public String produceCoffee() {
    // return “Coffee is made by SimpleCoffeeMachine”;
    // }

    // 升级后
    private CoffeeCooker mCoffeeCooker;
    
    public SimpleCoffeeMachine(CoffeeCooker coffeeCooker) {
        mCoffeeCooker = coffeeCooker;
    }
    @Override
    public String produceCoffee() {
        return mCoffeeCooker.produceCoffee();
    }
    

    }

基于目前的情况,我们制作咖啡的流程发生了变化,原来的业务随着CoffeeCooker的加入发生了改变,但细心的小伙伴会发现目前还有一个地方受到了影响,那就是我们的CoffeeShop,

public class CoffeeShop {

    private CoffeeMaker mCoffeeMaker;

//    public CoffeeShop(CoffeeMaker coffeeMachine) {
//        mCoffeeMachine = new SimpleCoffeeMachine();
//    }

    // 升级后
    public CoffeeShop(CoffeeCooker coffeeCooker) {
        mCoffeeMaker = new SimpleCoffeeMachine(coffeeCooker);
    }

    public String provideCoffee() {
        return mCoffeeMaker.produceCoffee();
    }
}

我们的SimpleCoffeeMachine升级了,业务波动影响到了我们的CoffeeShop,这时候不得不对CoffeeShop也进行修改。这时候我们的CoffeeShop就懵逼了,你SimpleCoffeeMachine业务升级就升级呗,为毛我制造CoffeeShop的过程也要变动,非常的不愿意,但迫于老板的压力。最后还是给整改了。很明显,这是一个不合适的流程,简单的一个业务的升级,还要我们对整个咖啡店进行修改,那如果业务非常复杂,引用了SimpleCoffeeMachine的可不仅仅是CoffeeShop一个,那是不是每个引用的地方都需要进行修改,业务庞大的情况下,这种修改就是致命的,不仅需要做大量没有意义的体力劳动来修改,还可能导致大片业务代码的变动直接增加测试的成本,其他接收这个需求的开发人员直接得跪键盘了,一个S SimpleCoffeeMachine的改动对CoffeeShop产生了直接的影响,肯定有什么地方是不对的。原因就是CoffeeShop里的SimpleCoffeeMachine是自己new出来的。这就是一个很不好的地方。这种糟糕的实例引用的方式我们称之为硬初始化(Hard init),和硬编码(Hard coding)一样,都是糟糕代码滋生的好方法,Hard init不仅增加了各个模块的耦合,还让单元测试变得更加困难了。

工厂模式

那么该用什么方法来尽量地降低各个模块的耦合,避免new对象带来的问题呢。我们知道,类的初始化可以描述成new,get和set,new就是我们上面说的Hard init,容易增加各个模块之间的耦合,而get,则可以看做是工厂模式,工厂模式是new的一个升级版本,相对硬初始化来说,工厂模式把对象的创建都集中在工厂里了,对于需要依赖的类来说,无需再考虑对象的创建工作了,只需要关注如何从工厂里获得,在发生修改时也不会有太多的改动,和以前的方案比起来要好了不少。但工厂模式的对象创建依然非常的不灵活,对象的实现完全取决于工厂,会导致原来的依赖由具体的对象变为依赖相应的工厂,本质上还是有依赖关系的!!!对,工厂模式并没有改变本质的依赖关系,而且,对于简单职责的工厂来说,抽出一层工厂似乎并不会太麻烦,但当我们的工厂中提供的类的实现复杂起来时,又回到了最初的问题上,我们是在工厂中new对象还是继续给工厂中的内容再提供一个工厂呢,这种层层嵌套会让我们的代码变得干涩难懂,也会有设计过度的嫌疑,工厂模式在解决依赖问题还是有点尴尬。

手动依赖注入

那么最后还有一个set,也就是我们的依赖注入了,依赖注入的依赖是从外部传递过来的,而且在Java平台上很多时候都是通过反射或者动态编译来提供依赖注入,这样就更加剥离的各个部分的耦合性,也让上述两种方式只能望其项背了。

依赖注入主要有三种途径:

public class CoffeeShopWithInjection implements InjectMaker {

    private CoffeeMaker mCoffeeMaker;

    // 第1种依赖注入方式: 构造器注入
    public CoffeeShopWithInjection(CoffeeMaker coffeeMaker) {
        mCoffeeMaker = coffeeMaker;
    }

    // 第2种依赖注入方式: Setter注入
    public void setCoffeeMaker(CoffeeMaker coffeeMaker) {
        mCoffeeMaker = coffeeMaker;
    }

    // 第3种依赖注入方式: 接口注入
    @Override
    public void injectMaker(CoffeeMaker coffeeMaker) {
        mCoffeeMaker = coffeeMaker;
    }

    public String produceCoffee() {
        return mCoffeeMaker.produceCoffee();
    }
}

InjectMarker接口内容如下:

public interface InjectMaker {
    void injectMaker(CoffeeMaker coffeeMaker);
}

依赖注入的最基本的原理比较容易理解,就是不要在需要依赖的类中通过new来创建依赖而是通过方法提供的参数注入进来,这样我们的需要依赖的类和提供依赖的类的实现方法分隔开了。不过这种手动提供依赖也是很繁杂的工作,充满的浓浓的重复体力劳动的气息,如何来尽量减少这些冗余代码的制作呢,答案就是下一部分。

依赖注入框架(Dagger2)

一般的IOC框架都是通过反射来实现的,但Dagger2作为Android端的IOC框架,为了不影响性能,它是通过apt动态生成代码来实现的.

Dagger2主要分为三个模块

  • 依赖提供方Module,负责提供依赖中所需要的对象,实际编码中类似于工厂类
  • 依赖需求方实例,它声明依赖对象,它在实际编码中对应业务类,例如Activity,当你在Activity中需要某个对象时,你只要在其中声明就行,声明的方法在下面会讲到
  • 依赖注入组件Component,负责将对象注入到依赖需求方,它在实际编码中是一个接口,编译时Dagger2会自动为它生成一个实现类

Dagger2的主要工作流程

  • 将依赖需求方实例传入给Component实现类
  • Component实现类根据依赖需求方实例中依赖声明,来确定该实例需要依赖哪些对象
  • 确定依赖对象后,Component会在与自己关联的Module类中查找有没有提供这些依赖对象的方法,有的话就将Module类中提供的对象设置到依赖需求方实例中

通俗上来讲就好比你现在需要一件衣服,自己做太麻烦了,你就去商店买,你跟商店老板说明你想要购买的类型后,商店老板就会在自己的衣服供应商中查找有没有你所说的类型,有就将它卖给你.其中你就对应上面所说的依赖需求方实例,你只要说明你需要什么,商店老板则对应Component实现类,负责满足别人的需求,而衣服供应商则对应Module类,他负责生产衣服.也许这里有点绕,但经过下面的Demo,也许能够帮助你理解.

在项目中引入Dagger2

在项目下的build.gradle文件中添加apt插件

buildscript {
    ...
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.2'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
        //添加apt插件
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}
...

在app目录的build.gradle文件中添加

apply plugin: 'com.android.application'
//应用apt插件,一定要放在下面
apply plugin: 'com.neenbedankt.android-apt'
...
dependencies {
    ...
    //引入dagger2
    compile 'com.google.dagger:dagger:2.4'
    apt 'com.google.dagger:dagger-compiler:2.4'
    //java注解
    provided 'org.glassfish:javax.annotation:10.0-b28'
}

Module类注入方式

编写布料类Cloth

写一个Cloth类用作依赖对象,它包含一个color属性

public class Cloth {
    private String color;

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    @Override
    public String toString() {
        return color + "布料";
    }
}

书写Module类

现在的需求是MainActivity中需要使用到Cloth对象,所以我们要为MainActivity书写一个Module类用来提供Cloth对象,相当于创建了一个提供商

@Module
public class MainModule {
    @Provides
    public Cloth getCloth() {
        Cloth cloth = new Cloth();
        cloth.setColor("红色");
        return cloth;
    }
}

嗯?怎么多了两个注解?这两个注解有什么用呢?
注解是Dagger2中的关键,编写Module类时要在该类上声明@Module以表明该类是Module类,这样Dagger2才能识别,那@Provides又是干嘛的呢?它的作用是声明Module类中哪些方法是用来提供依赖对象的,当Component类需要依赖对象时,他就会根据返回值的类型来在有@Provides注解的方法中选择调用哪个方法.在一个方法上声明@Provides注解,就相当于创建了一条生产线,这条生产线的产物就是方法的返回值类型.有了这条生产线,供应商就能提供这种类型的商品了,当商店老板(Component类)发现有人(依赖需求方)需要这种类型的商品时,供应商Module就可以提供给他了。

书写Component接口

@Component(modules = MainModule.class)
public interface MainComponent {
    void inject(MainActivity mainActivity);
}

和Module类一样,Component类也是需要注解声明的,那个注解就是@Component,但是@Component注解的作用可不是单单用来声明Component类,他还有更强大的功能,@Component注解有modules和dependencies两个属性,这两个属性的类型都是Class数组,modules的作用就是声明该Component含有哪几个Module,即有几个供应商,当Component需要某个依赖对象时,就会通过这些Module类中对应的方法获取依赖对象,MainComponent中只包含MainModule,所以令modules=MainModule.class,相当于供应商和商店老板确定合作关系的合同.而dependencies属性则是声明Component类的依赖关系,这个下面再详讲.

接口中那个方法又是干嘛用的呢?

我们现在只是声明了Component类,但我们要怎么将Component类和依赖需求方对象联合起来呢?答案就是通过这个inject方法,这个方法可以将依赖需求方对象送到Component类中,Component类就会根据依赖需求方对象中声明的依赖关系注入依赖需求方对象中所需要的对象,本Demo中MainActivity(依赖需求方)中需要Cloth对象,所以我们通过inject方法将MainActivity实例传入到MainComponent中,MainComponent就会从MainModule中的getCloth方法获取Cloth实例,并将该实例赋值给MainActivity中的cloth字段.相当于你去商店的道路,没有这条路,你就无法去商店和老板说明你所需要的东西.但是这里需要注意的是,inject方法的参数不能用子类来接收,例如本Demo中,如果inject的参数是Activity,那么Dagger2就会报错.

在MainActivity中声明

public class MainActivity extends AppCompatActivity {
    private TextView mTextView;
    @Inject
    Cloth mCloth;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTextView = (TextView) findViewById(R.id.tv);

        MainComponent build = DaggerMainComponent.builder()
                .mainModule(new MainModule())
                .build();
        build.inject(this);

        mTextView.setText("我现在有" + mCloth);
    }
}

上面代码中有两处关键:

  • 声明依赖对象Cloth,就是在cloth字段上添加@Inject注解,Dagger2中声明依赖对象都是通过@Inject注解,但是@Inject注解的字段不能是private和protected的.
  • 通过Dagger2自动生成的类来创建Component的实现类,创建时需要传入该Component实现类所需要的Module类实例,传入方法就是调用Module类类名首字母小写对应的方法.这里我们通过Dagger2自动生成的DaggerMainComponent类创建了MainComponent的实例,相当于我们创建了一个实实在在的商店,不再是理论上的商店,但是创建商店一定也要创建真实的供应商嘛,所以创建Component实现类时一定要传入Module的实例.(注意编写完Component接口后Dagger2并不会自动创建对应的类,需要我们点击Android Studio中bulid菜单下的Rebulid Poject选项,或者直接书写代码,编译时Dagger2就会帮你自动生成).
    再将MainActivity通过inject方法发送到MainComponent中,调用完inject方法后,你就会发现,MainActivity中的mCloth字段已经被赋值,而且该mCloth对应的就是我们在MainModule类getCloth方法中创建的Cloth对象.

运行结果

注解依赖对象注入方式

前面的例子可能给人最大的感受就是麻烦吧?就是为mCloth赋个值,又要写什么Module类,又是要写什么Component接口.其实Dagger2还可以用注解来提供依赖对象.让我们来瞧瞧怎么使用.

创建依赖类Shoe

我们又创建一个依赖类Shoe

public class Shoe {
    @Inject
    public Shoe() {}

    @Override
    public String toString() {
        return "鞋子";
    }
}

但是这次我们创建的方式和Cloth不一样了,我们在构造函数上声明了@Inject注解,这个注解有什么用呢?作用可大了,当Component在所拥有的Module类中找不到依赖需求方需要类型的提供方法时,Dagger2就会检查该需要类型的有没有用@Inject声明的构造方法,有则用该构造方法创建一个.

相当于你去商店购买东西,你需要的东西商店的供应商不生产,商店老板就只好帮你去网上看看有没有你需要的东西,有则帮你网购一个.

在MainActivity中声明Shoe依赖

我们修改之前的MainActivity,添加一点东西

public class MainActivity extends AppCompatActivity {
    private TextView mTextView;
    @Inject
    Cloth mCloth;
    @Inject
    Shoe mShoe;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTextView = (TextView) findViewById(R.id.tv);

        MainComponent build = DaggerMainComponent.builder()
                .mainModule(new MainModule())
                .build();
        build.inject(this);

        //mTextView.setText("我现在有" + mCloth);
        mTextView.setText("我现在有" + mCloth + "和" + mShoe);
    }
}

运行结果

注意

有些读者可能会这样想:为什么不都用这种方法来声明呢?为什么要用Module类?
答案是这样的,项目中我们会用到别人的jar包,我们无法修改别人的源码,就更别说在人家的类上添加注解了,所以我们只能通过Module类来提供.

复杂一点的使用情况

我们创建的这些依赖类不用依赖于其它依赖类,但是如果需要依赖于其它依赖类又要怎么弄呢?

创建依赖类Clothes

我们又来创建一个衣服类Clothes,制作衣服时需要布料,所以我们在创建Clothes的实例时需要用到Cloth实例(依赖到Cloth类)

public class Clothes {
    private Cloth cloth;
    public Clothes(Cloth cloth) {
        this.cloth = cloth;
    }
    public Cloth getCloth() {
        return cloth;
    }
    @Override
    public String toString() {
        return cloth.getColor() + "衣服";
    }
}

在Module类中增加提供方法

现在我们的MainActivity中需要依赖于Clothes对象,所以我们在MianModule中添加提供Clothes对象的方法,但是Clothes需要依赖于Cloth对象,这要怎么办呢?可能最先想到的办法就是这样:

@Provides
    public Clothes getClothes() {
        Cloth cloth = new Cloth();
        cloth.setColor("红色");
        return new Clothes(cloth);
}

直接在方法中创建一个Cloth不就得了,但是你有没有发现,创建Cloth的代码已经在getCloth方法中有了,我们能不能用getCloth方法中创建的Cloth实例来创建Clothes实例呢?

Dagger2提供了这样的功能,我们只要在getClothes方法中添加Cloth参数,Dagger2就会像帮依赖需求方找依赖对象一样帮你找到该方法依赖的Cloth实例,所以我们代码可以这样改:

@Provides
    public Clothes getClothes(Cloth cloth) {
        return new Clothes(cloth);
}

在MainActivity中声明Clothes依赖

public class MainActivity extends AppCompatActivity {
    ...
    @Inject
    Clothes clothes;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        tv.setText("我现在有" + cloth + "和" + shoe + "和" + clothes);
    }
}

运行结果

同理,在带有@Inject注解的构造函数要是依赖于其它对象,Dagger2也会帮你自动注入.

创建依赖类Shirt

public class Shirt {
    Cloth mCloth;
    @Inject
    public Shirt(Cloth cloth) {
        mCloth = cloth;
    }
    @Override
    public String toString() {
        return mCloth + "T恤";
    }
}

在MainActivity中声明Shirt依赖

public class MainActivity extends AppCompatActivity {
    ...
    @Inject
    Shirt mShirt;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mTextView.setText("我现在有" + mCloth + "和" + mShoe + "和" + mClothes + "和" + mShirt);
    }
}

运行结果

依赖总结

引用依赖注入神器:Dagger2详解系列中的一段话:
我们有两种方式可以提供依赖,一个是注解了@Inject的构造方法,一个是在Module里提供的依赖,那么Dagger2是怎么选择依赖提供的呢,规则是这样的:

  • 步骤1:查找Module中是否存在创建该类的方法
  • 步骤2:若存在创建类方法,查看该方法是否存在参数
    • 步骤2.1:若存在参数,则按从步骤1开始依次初始化每个参数
    • 步骤2.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束
  • 步骤3:若不存在创建类方法,则查找Inject注解的构造函数,看构造函数是否存在参数
    • 步骤3.1:若存在参数,则从步骤1开始依次初始化每个参数
    • 步骤3.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束

也就说Dagger2会递归的提供依赖

@Named和@Qulifier注解的使用

@Named

假设我们现在又有了新的需求,MainActivity中需要两种布料,分别是红布料和蓝布料,但我们的MainModule类中只能提供红布料,怎么办呢?
读者可能会想:在MainModule类中再添加一个提供蓝布料的方法不就行了:

@Provides
public Cloth getRedCloth() {
    Cloth cloth = new Cloth();
    cloth.setColor("红色");
    return cloth;
}
@Provides
public Cloth getBlueCloth() {
    Cloth cloth = new Cloth();
    cloth.setColor("蓝色");
    return cloth;
}

可问题就来了,Dagger2是通过返回值类型来确定的,当你需要红布料时,它又怎么知道哪个是红布料呢?所以Dagger2为我们提供@Named注解,它怎么使用呢?它有一个value值,用来标识这个方法是给谁用的.修改我们的代码:

@Provides
@Named("red")
public Cloth getRedCloth() {
    Cloth cloth = new Cloth();
    cloth.setColor("红色");
    return cloth;
}
@Provides
@Named("blue")
public Cloth getBlueCloth() {
    Cloth cloth = new Cloth();
    cloth.setColor("蓝色");
    return cloth;
}

我们在getRedCloth方法上使用@Named(“red”)表明此方法返回的是红布料,同理,在getBlueCloth方法上使用@Named(“blue”)表明此方法返回的是蓝布料,接下我们只要在MainActivity中的布料字段上同样使用@Named注解,就可以一一配对了.

public class MainActivity extends AppCompatActivity {
    ...
    @Inject
    @Named("red")
    Cloth redCloth;
    @Inject
    @Named("blue")
    Cloth blueCloth;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        tv.setText("我现在有" + mRedCloth + "和" + mBlueCloth );
    }
}

在redCloth上用@Named(“red”)标记后,他就会对应Module中对应的方法.

运行结果

@Qulifier

@Qulifier功能和@Named一样,并且@Named就是继承@Qulifier的,我们要怎么使用@Qulifier注解呢?答案就是自定义一个注解:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface RedCloth {
}

有了这个注解,我们就可以用它在替换掉上面的@Named(“red”),效果是一样的.

而且这两个注解还能使用在依赖参数上,比如这个:

@Provides
 public Clothes getClothes(@Named("blue") Cloth cloth){
     return new Clothes(cloth);
 }

效果和上面说明的一样,进入这个方法的cloth由上面有@Named(“blue”)的方法提供

@Singleton和@Scope的使用

@Singleton

假设现在MainActivity中需要依赖Clothes和Cloth,我们在MainModule中提供这两个类的提供方法:

@Provides
@Named("blue")
public Cloth getBlueCloth() {
    Cloth cloth = new Cloth();
    cloth.setColor("蓝色");
    return cloth;
}
@Provides
public Clothes getClothes(@Named("blue") Cloth cloth) {
    return new Clothes(cloth);
}

接着在MainActivity中声明

mTextView.setText("mBlueCloth=mClothes中的mBlueCloth吗?:" + (mBlueCloth == mClothes.getCloth()));

运行结果

你会发现,MainActivity中的Cloth对象Clothes中的Cloth对象不是同一个对象,注入过程中,对mBlueCloth注入时会调用一次getBlueCloth方法,创建了一个Cloth对象;注入mClothes时又会调用一次getBlueCloth方法,这时又会创建一个Cloth对象,所以才会出现上面的结果.但是如果需要MainActivity中的mBlueCloth对象和mClothes中的mBlueCloth对象是同一个对象又要怎么办呢?Dagger2为我们提供了@Singleton注解,和名字一样,这个注解的作用就是声明单例模式,我们先看看它怎么使用,下面再讲原理.

首先,在getRedCloth方法上添加该注解

@Provides
@Named("blue")
@Singleton
public Cloth getBlueCloth() {
    Cloth cloth = new Cloth();
    cloth.setColor("蓝色");
    return cloth;
}

再在MainComponent接口上添加该注解

@Singleton
@Component(modules = MainModule.class)
public interface MainComponent {
    void inject(MainActivity mainActivity);
}

运行结果

有没有发现,MainActivity中的Cloth对象和Clothes中的Cloth对象是同一个对象了,是不是很神奇!

Scope

@Singleton是怎么实现的呢?我们先看看@Scope注解,弄懂它,@Singleton你也就会明白了,下面我们就来分析分析
顾名思义,@Scope就是用来声明作用范围的.@Scope和@Qulifier一样,需要我们自定义注解才能使用,我们先自定义一个注解:

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PreActivity {
}

这个注解有什么用呢?答案就是声明作用范围,当我们将这个注解使用在Module类中的Provide方法上时,就是声明这个Provide方法是在PerActivity作用范围内的,并且当一个Component要引用这个Module时,必须也要声明这个Component是PerActivity作用范围内的,否则就会报错,声明方法也很简单,就是在Component接口上使用这个注解.但是我们声明这个作用范围又有什么用呢?

原来Dagger2有这样一个机制:在同一个作用范围内,Provide方法提供的依赖对象就会变成单例,也就是说依赖需求方不管依赖几次Provide方法提供的依赖对象,Dagger2都只会调用一次这个方法.就和上面那个例子一样,正常情况下,在注入MainActivity中的mBlueCloth对象时会调用一次getBlueCloth方法,注入mClothes对象时因为依赖Cloth对象,所以又会调用一次getBlueCloth方法,导致这两个Cloth对象并不是同一个实例.但是我们给它声明作用范围后,这两次对Cloth的依赖只会调用一次getBlueCloth方法,这样这两个Cloth对象就是同一实例了,这样就保证了在给MainActivity注入时,所有声明的Cloth依赖都是指向同一个实例.(注意:只有Module类中声明了作用范围的Provide方法才能实现单例,没声明的方法就不是单例的)

查看源码你会发现Singleton其实是继承@Scope注解的,所以你知道了Singleton是怎么实现单例模式的吧.

可能有些读者可能会问,Dagger2既然有了Singleton为什么还要我们自定义PerActivity注解?这就涉及到代码可读性了,当依赖需求方是Activity时,我们可以自定义一个PerActivity注解,当依赖需求方是Fragment时,我们又可以自定义一个PerFragment注解,这样我们就能清楚的区分依赖对象的提供目标了.

那我们通过构造函数提供依赖的方式又要怎么声明作用范围呢?答案就是在类名上使用注解标明,切记不要在构造函数上用注解标明,这样是无效的.
读者可以试试用PerActivity注解代替上面例子中的Singleton注解,你会发现效果是一样的.

注意注意注意:单例是在同一个Component实例提供依赖的前提下才有效的,不同的Component实例只能通过Component依赖才能实现单例.也就是说,你虽然在两个Component接口上都添加了PerActivity注解,但是这两个Component提供依赖时是没有联系的,他们只能在各自的范围内实现单例.(下面会降到)

组件依赖dependencies的使用

在实际开发中,我们经常会使用到工具类,工具类一般在整个App的生命周期内都是单例的,我们现在给我们的Demo添加一个工具类ClothHandler:

public class ClothHandler {
    public Clothes handle(Cloth cloth){
        return new Clothes(cloth);
    }
}

它的功能就是将cloth加工成clothes,假设我们现在有两个Activity中都要使用该工具类,我们要怎么使用Dagger2帮我们注入呢?

我们先用上面所学的方法试试,先在MainModule中添加提供方法:

@Module
public class MainModule {
     .....
    @PerActivity
    @Provides
    public ClothHandler getClothHandler(){
        return new ClothHandler();
    }
}

再在MainActivity中声明依赖

public class MainActivity extends AppCompatActivity {
        .....    
    @Inject
    ClothHandler mClothHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ......
    mTextView.setText("红布料加工后变成了" + mClothHandler.handle(mRedCloth) + "\nmClothHandler地址:" + mClothHandler);
    }
    //在布局文件中声明的点击方法
    public void onclick(View view) {
        Intent intent = new Intent(this,SecondActivity.class);
        startActivity(intent);
    }
}

同理在书写第二个Activity,并为它书写Module类Component接口

public class SecondActivity extends AppCompatActivity {
    private TextView tv;
    @Inject
    Cloth mBlueCloth;
    @Inject
    ClothHandler mClothHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        tv = (TextView) findViewById(R.id.tv2);
        SecondComponent component = DaggerSecondComponent.builder()
                .secondModule(new SecondModule())
                .build();
        component.inject(this);
        tv.setText("蓝布料加工后变成了" + mClothHandler.handle(mBlueCloth) + "\nmClothHandler地址:" + mClothHandler);
    }
}

@Module
public class SecondModule {
    @PreActivity
    @Provides
    public Cloth getBlueCloth(){
        Cloth cloth = new Cloth();
        cloth.setColor("蓝色");
        return cloth;
    }
    @PreActivity
    @Provides
    public ClothHandler getClothHandler(){
        return new ClothHandler();
    }
}

@Component(modules = SecondModule.class)
@PreActivity
public interface SecondComponent {
    void inject(SecondActivity secondActivity);
}

运行结果

你会发现,虽然我们成功的将ClothHandler注入到了这两个Activity中,但是你会发现,这两个Activity中的ClothHandler实例不是一样的(验证了上面那个结论),并且我们发现这种注入方式要在每一个Module中都要提供getClothHandler方法, 假如有20个Activity都需要用到ClothHandler,那我们都这样写,不就代码重复了吗.并且我们还要实现单例,怎么办呢?Dagger2很贴心的为我们提供了Component依赖,就能完美的解决这个问题.

在面向对象的思想中,我们碰到这种情况一般都要抽取父类,Dagger2也是用的这种思想,我们先创建一个BaseModule,用来提供工具类:

@Module
public class BaseModule {
    @Singleton //单例
    @Provides
    public ClothHandler getClothHandler(){
        return new ClothHandler();
    }
}

在创建一个BaseComponent接口:

@Singleton
@Component(modules = BaseModule.class)
public interface BaseComponent {
    ClothHandler getClothHandler();
}

嗯?

这个Component怎么有点不一样,怎么没有inject方法呢?上面讲过,我们通过inject方法依赖需求方实例送到Component中,从而帮助依赖需求方实现依赖,但是我们这个BaseComponent是给其他Component提供依赖的,所以我们就可以不用inject方法,但是BaseComponent中多了一个getClothHandler方法,它的返回值是ClothHandler对象,这个方法有什么用呢?它的作用就是告诉依赖于BaseComponent的Component,BaseComponent能为你们提供ClothHandler对象,如果没有这个方法,BaseComponent就不能提供ClothHandler对象(这个提供规则和上面的依赖规则相同,可以实现单例).既然有了BaseComponent,那我们就可在其它Component中依赖它了.我们删除MainModule和SecondModule中的getClothHandler方法即可。

接下来在MainComponent和SecondComponent中声明依赖,就要用到@Component中的dependencies属性了:

@PreActivity
@Component(modules=MainModule.class,dependencies = BaseComponent.class)
public interface MainComponent {
    void inject(MainActivity mainActivity);
}

@PreActivity
@Component(modules = SecondModule.class,dependencies = BaseComponent.class)
public interface SecondComponent {
    void inject(SecondActivity secondActivity);
}

下面我们用Android Studio中build菜单下的Rebuild Object选项后,你会发现创建MainComponent和SecondComponent实例时多了一个baseComponent方法:

这个方法需要我们传入一个BaseComponent实例,原因很简单,MainComponent和SecondComponent既然依赖BaseComponent,肯定需要你传入一个BaseComponent实例给它,它才能从BaseComponent实例中获取到它需要的对象嘛.但是需要注意的是,如果要MainComponent和SecondComponent依赖到的对象是同一个的话(也就是单例),创建它们是传入的BaseComponent实例也必须是同一个,上面说过,不同的Component实例是无法提供相同的依赖实例的,因为它们之间是没有联系的.这样的话,我们就需要在MainActivity和SecondActivity中能获取到同一个BaseComponent实例,怎么样能实现呢?很多人一开始都会想到用静态工厂,这种方法可行,但是我们一般都会自定义一个Application类,用它来提供BaseComponent实例,因为在整个App生命周期内都只有一个Application实例,所以其中的BaseComponent实例也不会变.我们自定义一个MyApplication类

public class MyApplication extends Application {
    private BaseComponent baseComponent;
    @Override
    public void onCreate() {
        super.onCreate();
        baseComponent = DaggerBaseComponent.builder().baseModule(new BaseModule()).build();
    }

    public BaseComponent getBaseComponent() {
        return baseComponent;
    }
}

我们在onCreate方法中创建BaseComponent实例,并对外提供获取方法.
这种方式还有一种好处,就是当我们在BaseModule中需要用到Application实例时,我们就可以在创建BaseModule时传入this.

接下来在AndroidManifest.xml中声明我们新建的MyApplication

接下来修改MainActivity和SecondActivity中的代码

MainComponent build = DaggerMainComponent
                .builder()
                .baseComponent(((MyApplication)getApplication()).getBaseComponent())
                .mainModule(new MainModule())
                .build();

SecondComponent component = DaggerSecondComponent
                .builder()
                .baseComponent(((MyApplication)getApplication()).getBaseComponent())
                .secondModule(new SecondModule())
                .build();

运行结果

我们成功的将ClothHandler注入到了这两个Activity中,并且还实现了单例.(注意:这里能实现单例跟BaseComponent中声明了@Singleton有很大关系,因为BaseComponent都没有单例的话,外部依赖它的Component就更不可能单例了).

Component的生命周期

一般情况下我们都是在Activity的onCreate方法中创建Component实例,再调用inject方法完成依赖.所以Component依赖可以分为三个过程:

  • 创建Component实例

    MainComponent component = DaggerMainComponent

    .builder()
    .baseComponent(((MyApplication)getApplication()).getBaseComponent())
    .mainModule(new MainModule())
    .build();
    
  • 调用inject方法

    component.inject(this);

调用完这个方法整个依赖就完成了.

  • Component实例被销毁

onCreate()方法调用完成后,Component实例就会因为没有被引用而被垃圾回收器回收.其中传入给Component实例的Module实例也会一同被回收,这也就能说明不同的Component实例之间是没有联系的(Component依赖除外).

总结

至此,Dagger2基础已讲完,对于Dagger2在项目中的使用方法,可以参考github上的开源项目.

最后我们引用一下Dagger2 Scope 注解能保证依赖在 component 生命周期内的单例性吗?中的注意事项:

  • component 的 inject 函数不要声明基类参数;
  • Scope 注解必须用在 module 的 provide 方法上,否则并不能达到局部单例的效果;
  • 如果 module 的 provide 方法使用了 scope 注解,那么 component 就必须使用同一个注解,否则编译会失败;
  • 如果 module 的 provide 方法没有使用 scope 注解,那么 component 和 module 是否加注解都无关紧要,可以通过编译,但是没有局部单例效果;
  • 对于直接使用 @Inject 构造函数的依赖,如果把 scope 注解放到它的类上,而不是构造函数上,就能达到局部单例的效果了;
  • 被依赖的Component能提供某个对象时,一定要在接口中声明以该对象为返回值的方法(也就是暴露接口).这样依赖它的Component才能获取到这种对象.

源码入口

参考资料

Google官方介绍
依赖注入神器:Dagger2详解系列
Dagger2 入门,以初学者角度
Dagger2图文完全教程
Dagger2 Scope 注解能保证依赖在 component 生命周期内的单例性吗?
GeekNews

QinPeng Zhu wechat
扫一扫,关注我的公众号获取更多资讯!
学习分享,感谢鼓励!